using UnityEngine;
using System.Collections;

// Require a character controller to be attached to the same game object
[RequireComponent(typeof(CharacterController))]
[AddComponentMenu("Third Person Player/Third Person Controller")]

public class ThirdPersonController : MonoBehaviour
{
	//Speed while walking
	public float walkSpeed = 3.0f;
	
	//After trotAfterSeconds trot at trotSpeed
	public float trotSpeed = 4.0f;
	//While pressing "Fire3" begin run
	public float runSpeed = 6.0f;
	
	public float inAirControlAcceleration = 3.0f;
	
	//Height of initial jump
	public float jumpHeight = 0.5f;
	
	//Extra height added when jump button held longer
	public float extraJumpHeight = 2.5f;
	
	//Gravity
	public float gravity = 20.0f;
	
	//Gravity in controlled descent
	public float controlledDescentGravity = 2.0f;
	public float speedSmoothing = 10.0f;
	public float rotateSpeed = 500.0f;
	public float trotAfterSeconds = 3.0f;
	
	public bool canJump = true;
	public bool canControlDescent = true;
	public bool canWallJump = false;
	
	private float jumpRepeatTime = 0.05f;
	private float wallJumpTimeout = 0.15f;
	private float jumpTimeout = 0.15f;
	private float groundedTimeout = 0.25f;
	
	//Camera has a slight delay in following target
	private float lockCameraTimer = 0.0f;
	
	//Current move direction in x-z
	private Vector3 moveDirection = Vector3.zero;
	//Current vertical speed
	private float verticalSpeed = 0.0f;
	//Current x-z move speed
	private float moveSpeed = 0.0f;
	
	//The last collision flags returned from controller.Move
	private CollisionFlags collisionFlags;
	
	//Is character jumping
	private bool jumping = false;
	private bool jumpingReachedApex = false;
	
	//Is character moving backwards (lock Camera)
	private bool movingBack = false;
	//Is the user pressing keys?
	private bool isMoving = false;
	//When did character begin walking (used for trot)
	private float walkTimeStart = 0.0f;
	//Last time jump button was clicked down
	private float lastJumpButtonTime = -10.0f;
	//Last time jumped
	private float lastJumpTime = -1.0f;
	//Avergae normal of last touched geometry
	private Vector3 wallJumpContactNormal;
	private float wallJumpContactNormalHeight;
	
	//Height jumped from
	private float lastJumpStartHeight = 0.0f;
	//When was the wall first touched during this jump
	private float touchWallJumpTime = -1.0f;
	
	private Vector3 inAirVelocity = Vector3.zero;
	
	private float lastGroundedTime = 0.0f;
	//private float lean = 0.0f;
	private bool slammed = false;
	
	private bool isControllable = true;
	
	//Initializes moveDirection to (0,0,1)
	void Awake()
	{
		moveDirection = transform.TransformDirection(Vector3.forward);
	}
	
	void HidePlayer()
	{
		GameObject.Find("rootJoint").GetComponent<SkinnedMeshRenderer>().enabled = false;
		//disable player controls
		isControllable  = false;
	}	
	
	void ShowPlayer()
	{
		//Start rendering player again
		GameObject.Find("rootJoint").GetComponent<SkinnedMeshRenderer>().enabled = true;
		//All player to control character again
		isControllable = true;
	}
	
	void UpdateSmoothedMovementDirection()
	{
		Transform cameraTransform = Camera.main.transform;
		bool grounded = IsGrounded();
	
		//Forward vector relative to the camera along the x-z plane
		Vector3 forward = cameraTransform.TransformDirection(Vector3.forward);
		//Zero y component of direction
		forward.y = 0f;
		//Set magnitude of vector to 1
		forward = forward.normalized;
		//Debug.Log(forward);
		//Right vector relative to the camera
		//Always orthogonal to the forward vector
		Vector3 right = new Vector3(forward.z, 0, -forward.x);
		right = right.normalized;
		//Debug.Log(right);
		
		float v = Input.GetAxisRaw("Vertical");
		float h = Input.GetAxisRaw("Horizontal");
		
		//h and v have values of 1 or -1, 0 if no input
		
		//Moving backwards or looking backwards
		if(v < -0.2f)
		{
			movingBack = true;
		}
		else
		{
			movingBack = false;
		}
		
		//Every update, set Was Moving to the current value of isMoving
		bool wasMoving = isMoving;
		//If input (-1/1) isMoving = true
		isMoving = (Mathf.Abs(h) > 0.1f || Mathf.Abs(v) > 0.1f);

		//Target direction relative to the camera
		Vector3 targetDirection = Vector3.Normalize(h * right) + Vector3.Normalize(v * forward);
		
		//Debug Tools
		Debug.DrawRay(cameraTransform.position, targetDirection, Color.red);
		//Grounded controls
		if(grounded)
		{
			//Lock the camera for a short period between moving & standing still
			lockCameraTimer += Time.deltaTime;
			if(isMoving != wasMoving)
			{
				lockCameraTimer = 0.0f;
			}
			
			//Store speed and direction separately
			//So that when the character is still, have a valid forward
			//moveDirection is always normalized, only update on user input
			if(targetDirection != Vector3.zero)
			{
				//If slow, snap to target direction
				if(moveSpeed < walkSpeed * 0.9f && grounded)
				{
//					Debug.Log("Move Direction" + moveDirection);
//					Debug.Log("Target Direction" +targetDirection);
					moveDirection = targetDirection.normalized;
				}
				//Otherwise smoothly turn towards it
				else
				{
					moveDirection = Vector3.RotateTowards(moveDirection, targetDirection, rotateSpeed * Mathf.Deg2Rad * Time.deltaTime, 1000);
					
					moveDirection = moveDirection.normalized;
				}
			}
			
			//Smooth the speed based on the current target direction
			float curSmooth = speedSmoothing * Time.deltaTime;
			
			//Choose target speed
			//Support analog input, ensuring faster diagonal movement is impossible
			float targetSpeed = Mathf.Min(targetDirection.magnitude, 1.0f);
			
			//Pick speed modifier
			if(Input.GetButton("Fire3"))
			{
				targetSpeed *= runSpeed;
			}
			else if(Time.time - trotAfterSeconds > walkTimeStart)
			{
				targetSpeed *= walkSpeed;
			}
			else
			{
				targetSpeed *= walkSpeed;
			}
			
			moveSpeed = Mathf.Lerp(moveSpeed, targetSpeed, curSmooth);
			
			//Reset walk time start when slowing
			if(moveSpeed < walkSpeed * 0.3f)
			{
				walkTimeStart = Time.time;
			}
			
		}
		else
		{
			//Lock camera while in the air
			if(jumping)
			{
				lockCameraTimer = 0.0f;
			}
			
			if(isMoving)
			{
				inAirVelocity += targetDirection.normalized * Time.deltaTime * inAirControlAcceleration;
			}
		}
	}
	
	void ApplyWallJump()
	{
		//Must jump against a wall for this to work 
		if(!jumping)
		{
			return;
		}
		
		//Store when first touched the wall during jump
		if(collisionFlags == CollisionFlags.CollidedSides)
		{
			touchWallJumpTime = Time.time;
		}
		
		//The user can trigger a wall jump shortly after hitting a wall the first time
		bool mayJump = lastJumpButtonTime > touchWallJumpTime - wallJumpTimeout && lastJumpButtonTime < touchWallJumpTime + wallJumpTimeout;
		if(!mayJump)
		{
			return;
		}
		
		//Prevent jumping too quickly in a row
		if(lastJumpTime + jumpRepeatTime > Time.time)
		{
			return;
		}
		
		if(Mathf.Abs(wallJumpContactNormal.y) < 0.2f)
		{
			wallJumpContactNormal.y = 0;
			moveDirection = wallJumpContactNormal.normalized;
			//Wall jump gives at least trotspeed
			moveSpeed = Mathf.Clamp(moveSpeed * 1.5f, trotSpeed, runSpeed);
		}
		else
		{
			moveSpeed = 0;
		}
		
		verticalSpeed = CalculateJumpVerticalSpeed(jumpHeight);
		DidJump();
		SendMessage("DidWallJump", null, SendMessageOptions.DontRequireReceiver);
	}
	
	public void ApplyJumping ()
	{
		// Prevent jumping too fast after each other
		if(lastJumpTime + jumpRepeatTime > Time.time)
		{
			return;
		}
		if(IsGrounded()) 
		{
			// Jump
			// - Only when pressing the button down
			// - With a timeout so you can press the button slightly before landing		
			if (canJump && Time.time < lastJumpButtonTime + jumpTimeout)
			{
				verticalSpeed = CalculateJumpVerticalSpeed(jumpHeight);
				SendMessage("DidJump", SendMessageOptions.DontRequireReceiver);
			}
		}
	}
	
	public void ApplyGravity()
	{
		//Do not move player if not controllable
		if(isControllable)
		{
			//Apply Gravity
			bool jumpButton = Input.GetButton("Jump");
			
			//When falling, use controlledDescentGravity when holding jump
			bool controlledDescent = ((canControlDescent) && (verticalSpeed <= 0.0f) && (jumpButton) && (jumping));
		
			//When the character reaches the apex of the jump send a message
			if(jumping && !jumpingReachedApex && verticalSpeed <= 0.0f)
			{
				jumpingReachedApex = true;
				SendMessage("DidJumpReachApex", SendMessageOptions.DontRequireReceiver);
			}
			
			//When jumping do not apply gravity for some time when the user is holding button
			//Gives greater control over jump height by pressing button longer
			bool extraPowerJump = (IsJumping() && verticalSpeed > 0.0f && jumpButton && transform.position.y < lastJumpStartHeight + extraJumpHeight);
			if(controlledDescent)
			{
				verticalSpeed -= controlledDescentGravity * Time.deltaTime;
			}
			else if(extraPowerJump)
			{
				return;
			}
			else if(IsGrounded())
			{
				verticalSpeed = 0.0f;
			}	
			else
			{
				verticalSpeed -= gravity * Time.deltaTime;
			}
		}
		
	}
	
	public float CalculateJumpVerticalSpeed(float targetJumpHeight)
	{
		return Mathf.Sqrt( 2 * targetJumpHeight * gravity);
	}
	
	void DidJump()
	{
		jumping = true; 
		jumpingReachedApex = false;
		lastJumpTime = Time.time;
		lastJumpStartHeight = transform.position.y;
		touchWallJumpTime = -1.0f;
		lastJumpButtonTime = -10.0f;
	}
	
	void Update()
	{
		if(!isControllable)
		{
			//Kill all inputs if not controllable
			Input.ResetInputAxes();
		}
		
		if(Input.GetButtonDown("Jump"))
		{
			lastJumpButtonTime = Time.time;
		}
		
		UpdateSmoothedMovementDirection();
		
		//Apply Gravity
		//Extra power jump modifies gravity
		//controlledDescent mode modifies gravity
		ApplyGravity();
		
		//Perform a wall jump logic
		//Make sure against a wall
		//Apply jump in correct direction
		if(canWallJump)
		{
			ApplyWallJump();
		}
		
		//Apply jumping logic
		ApplyJumping();
		
		//Calculate motion
		Vector3 movement = moveDirection * moveSpeed + new Vector3(0, verticalSpeed, 0) + inAirVelocity;
		
		movement *= Time.deltaTime;
		
		//Move the controller
		CharacterController controller = GetComponent<CharacterController>();
		wallJumpContactNormal = Vector3.zero;
		collisionFlags = controller.Move(movement);
		
		//Set rotation to the move direction
		if(IsGrounded())
		{
			//If knocked over, reset some values
			if(slammed)
			{
				slammed = false;
				controller.height = 2.0f;
				transform.position += new Vector3(0, 0.75f, 0);
			}
			
			transform.rotation = Quaternion.LookRotation(moveDirection);
		}
		else
		{
			if(!slammed)
			{
				Vector3 xzMove = movement;
				xzMove.y = 0;
				if(xzMove.sqrMagnitude > 0.001f)
				{
					transform.rotation = Quaternion.LookRotation(xzMove);
				}
			}
		}
		
		//In jump mode but became grounded
		if(IsGrounded())
		{
			lastGroundedTime = Time.time;
			inAirVelocity = Vector3.zero;
			if(jumping)
			{
				jumping = false;
				SendMessage("DidLand", SendMessageOptions.DontRequireReceiver);
			}
		}
	}
	
	void OnControllerColliderHit(ControllerColliderHit hit)
	{
		//Debug.DrawRay(hit.point, hit.normal)
		if(hit.moveDirection.y > 0.01f)
		{
			return;
		}
		
		wallJumpContactNormal = hit.normal;
	}
	
	public float GetSpeed()
	{
		return moveSpeed;
	}
	
	public bool IsJumping()
	{
		return (jumping && !slammed);
	}
	
	public bool IsGrounded()
	{
		return ((collisionFlags & CollisionFlags.CollidedBelow) != 0);
	}
	
	public void SuperJump(float height)
	{
		verticalSpeed = CalculateJumpVerticalSpeed(height);
		collisionFlags = CollisionFlags.None;
		SendMessage("DidJump", SendMessageOptions.DontRequireReceiver);
	}
	
	public void SuperJump(float height, Vector3 jumpVelocity)
	{
		verticalSpeed = CalculateJumpVerticalSpeed(height);
		inAirVelocity = jumpVelocity;
		
		collisionFlags = CollisionFlags.None;
		SendMessage("DidJump", SendMessageOptions.DontRequireReceiver);
	}
	
	public Vector3 GetDirection()
	{
		return moveDirection;
	}
	
	public bool IsMovingBackwards()
	{
		return movingBack;
	}
	
	public float GetLockCameraTimer()
	{
		return lockCameraTimer;
	}
	
	public bool IsMoving()
	{
		return Mathf.Abs(Input.GetAxisRaw("Vertical")) + Mathf.Abs(Input.GetAxisRaw("Horizontal")) > 0.5f;
	}
	
	public bool HasJumpReachedApex()
	{
		return jumpingReachedApex;
	}
	
	public bool IsGroundedWithTimeout()
	{
		return lastGroundedTime + groundedTimeout > Time.time;
	}
	
	public bool IsControlledDescent()
	{
		//When falling use controlledDescentGravity
		bool jumpButton = Input.GetButton("Jump");
		return (canControlDescent && verticalSpeed <= 0.0f) && (jumpButton && jumping);
	}
	
	void Reset()
	{
		gameObject.tag = "Player";
	}
}
